﻿using System;
using System.Linq;

namespace MLSharp
{
	/// <summary>
	/// Represents an instance from a data set.
	/// </summary>
	public class Instance : ICloneable
	{
		#region Public Properties

		/// <summary>
		/// Gets the attributes of the instance.
		/// </summary>
		public double[] Values { get; internal set; }

		/// <summary>
		/// The DataSet the instance belongs to.
		/// </summary>
		/// <remarks>
		/// This must be populated by the <see cref="IDataSet"/> class.
		/// </remarks>
		public IDataSet DataSet { get; internal set; }

		/// <summary>
		/// The weight assigned to the instance (default is 1.0).
		/// </summary>
		public double Weight { get; set; }

		/// <summary>
		/// The label for the instance.
		/// </summary>
		public string Label { get; set; }

		/// <summary>
		/// Gets the value for the class attribute, if any.
		/// </summary>
		/// <exception cref="InvalidOperationException">Thrown if the instance belongs to a 
		/// <see cref="DataSet"/> that doesn't have a class index specified.</exception>
		public string ClassValue
		{
			get
			{
				if (DataSet == null || DataSet.TargetAttributeIndex < 0)
				{
					throw new InvalidOperationException("Instance doesn't belong to an IDataSet that has a class index specified.");
				}

				//If the class value is missing, return an empty string.
				if (IsValueMissing(DataSet.TargetAttributeIndex))
				{
					return "";
				}
				//Otherwise, return the class value.
				else if (DataSet.Attributes[DataSet.TargetAttributeIndex].IsNumeric())
				{
					return Values[DataSet.TargetAttributeIndex].ToString();
				}
				else
				{
					return GetStringValue(DataSet.TargetAttributeIndex);
				}
			}
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Constructs a new instance with the specified values and label.
		/// </summary>
		/// <param name="label"></param>
		/// <param name="values"></param>
		public Instance(string label, params double[] values)
		{
			Label = label;

			Weight = 1.0;

			Values = values;
		}

		/// <summary>
		/// Constructs a new instance with the specified values
		/// </summary>
		/// <param name="values"></param>
		public Instance(params double[] values) : this("", values)
		{
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Converts the instance to a friendly string representation.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			//If the instance has no dataset, we can't create a string version of it.
            if (DataSet == null)
            {
            	throw new InvalidOperationException(
            		"Instance is not associated to a dataset, can't represent it as a string.");
            }

			//Create a string to hold the value of each attribute
			string[] values = new string[DataSet.Attributes.Count];

			//Convert each attribute value to a string
			for (int i=0; i < values.Length; i++)
			{
				//If the value is missing, write a null
				if (IsValueMissing(i))
				{
					values[i] = null;
				}
				//Else if it's numeric, just conver the value to a string.
				else if (DataSet.Attributes[i].IsNumeric())
				{
					values[i] = Values[i].ToString();
				}
				//Otherwise, the value should match something in the list of possible
				//values for the attribute.
				else
				{
					values[i] = GetStringValue(i);
				}
			}
			
			//Now convert everything to a string, writing the ID first.
			return string.Format("{{'{0}',{1}}}", Label, string.Join(",", values.Select(value => "'" + value + "'").ToArray()));
		}

		/// <summary>
		/// Returns true if the specified value is missing from the instance.
		/// </summary>
		/// <param name="index">The index of the value.</param>
		/// <returns>True if the value is missing, false otherwise.</returns>
		public bool IsValueMissing(int index)
		{
			//double.NaN is now our marker for a missing attribute.
			return double.IsNaN(Values[index]);
		}

		/// <summary>
		/// Creates a copy of the instance.
		/// </summary>
		/// <returns></returns>
		public Instance Clone()
		{
			return new Instance(Label, (double[])Values.Clone());
		}

		/// <summary>
		/// Routes calls through the <see cref="ICloneable"/> interface to the real
		/// Clone method.
		/// </summary>
		/// <returns></returns>
		object ICloneable.Clone()
		{
			return Clone();
		}

		/// <summary>
		/// Sets the string value for the specified attribute.
		/// </summary>
		/// <param name="index">The index of the set attribute value to set.</param>
		/// <param name="value">The new value.</param>
		/// <exception cref="InvalidOperationException">Thrown if the instance doesn't belong to an <see cref="IDataSet"/>.</exception>
		/// <exception cref="ArgumentException">Thrown if the attribute at the specified index isn't a <see cref="AttributeType.Set"/> attribute.</exception>
		public void SetValue(int index, string value)
		{
			//If the instance doesn't belong to a dataset, throw an exception
			if (DataSet == null)
			{
				throw new InvalidOperationException("Instance must be associated to an IDataSet in order to use this method.");
			}

			//If the attribute isn't a set attribute, explode
			if (DataSet.Attributes[index].Type != AttributeType.Set)
			{
				throw new ArgumentException("Attribute at specified index is not a Set attribute.");
			}

			//Store the index of the target value.
			int valueIndex = Array.IndexOf(DataSet.Attributes[index].PossibleValues, value);

			//If the specified value doesn't exist in the list of possible values, explode.
			if (valueIndex < 0)
			{
				throw new ArgumentException("The specified value is not a valid value for the specified attribute.");
			}

			Values[index] = valueIndex;
		}

		/// <summary>
		/// Gets the value of the specified Set attribute.
		/// </summary>
		/// <param name="index"></param>
		/// <returns></returns>
		/// <exception cref="ArgumentException">Thrown if the index doesn't correspond to a Set attribute.</exception>
		/// <exception cref="InvalidOperationException">Thrown if the instance doesn't have a <see cref="IDataSet"/>.</exception>
		public string GetStringValue(int index)
		{
			//If the instance doesn't have a dataset, explode.
			if (DataSet == null)
			{
				throw new InvalidOperationException("Instance must belong to a IDataSet in order to use this method.");
			}

			//If the attribute isn't a set attribute, explode.
			if (DataSet.Attributes[index].Type != AttributeType.Set)
			{
				throw new ArgumentException("Attribute at specified index is not a Set attribute.");
			}

			return DataSet.Attributes[index].PossibleValues[(int) Values[index]];
		}

		#endregion
	}
}
